Mapas en acción. Es más complejo

Author

Tomás Bustos

Mapas en acción: Análisis y visualización espacial con R

3. Es más complejo

En este encuentro se buscará incorporar instrumentos más avanzados al momento de visualizar información geográfica.

3.1. Pregunta-problema

El objetivo será analizar las elecciones en la Provincia de Buenos Aires a nivel de sección electoral. ¿Dónde le fue mejor y peor a cada partido? ¿Qué región contribuyó más a la fuerza ganadora?

Cargamos las librerías.

Code
library(tidyverse)
library(sf)

3.2. ¿Qué puede un geojson?

Primero hay que armar el objeto geográfico con resultados electorales.

Code
res <- readxl::read_excel(params$ruta_pba)

dim(res)
[1] 1080   10
Code
geo <- st_read(params$ruta_partidos) %>% 
  mutate(id = paste0("BUENOS AIRES_",str_to_upper(nam)), 
         id = stringi::stri_trans_general(id, "Latin-ASCII"),
         prov="PBA") 
Reading layer `partidos' from data source 
  `C:\Users\tomas.bustos\Documents\personal\estacion-r\Mapas en accion\geo\partidos.geojson' 
  using driver `GeoJSON'
Simple feature collection with 143 features and 8 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -63.39 ymin: -41.04 xmax: -56.66 ymax: -33.26
Geodetic CRS:  WGS 84
Code
geo %>% ggplot()+geom_sf()

Chequamos claves de unión.

Code
geo_check <- geo %>% st_drop_geometry() %>% select(id) %>% distinct(id) %>% mutate(base_geo=1)

geo_check <- res %>% 
  select(id) %>% 
  distinct(id) %>% 
  mutate(base_res=1) %>% 
  full_join(geo_check, by="id") %>% 
  filter(is.na(base_res) | is.na(base_geo)) 

geo_check
# A tibble: 18 × 3
   id                                              base_res base_geo
   <chr>                                              <dbl>    <dbl>
 1 BUENOS AIRES_25 DE MAYO                                1       NA
 2 BUENOS AIRES_9 DE JULIO                                1       NA
 3 BUENOS AIRES_CAÑUELAS                                  1       NA
 4 BUENOS AIRES_CORONEL DE MARINA LEONARDO ROSALES        1       NA
 5 BUENOS AIRES_GENERAL JUAN MADARIAGA                    1       NA
 6 BUENOS AIRES_NUEVE DE JULIO                           NA        1
 7 BUENOS AIRES_CANUELAS                                 NA        1
 8 BUENOS AIRES_GENERAL MADARIAGA                        NA        1
 9 BUENOS AIRES_VEINTICINCO DE MAYO                      NA        1
10 BUENOS AIRES_ISLAS BARADERO                           NA        1
11 BUENOS AIRES_ISLAS DE ZARATE                          NA        1
12 BUENOS AIRES_ISLAS SAN FERNANDO                       NA        1
13 BUENOS AIRES_ISLAS RAMALLO                            NA        1
14 BUENOS AIRES_ISLAS SAN PEDRO                          NA        1
15 BUENOS AIRES_ISLAS CAMPANA                            NA        1
16 BUENOS AIRES_CORONEL ROSALES                          NA        1
17 BUENOS AIRES_ISLAS TIGRE                              NA        1
18 BUENOS AIRES_ISLAS DE SAN NICOLAS                     NA        1

Reemplazamos y unimos.

Code
geo <- geo %>% 
  mutate(id = str_replace(id, "BUENOS AIRES_NUEVE DE JULIO", "BUENOS AIRES_9 DE JULIO"),
         id = str_replace(id, "BUENOS AIRES_CANUELAS", "BUENOS AIRES_CAÑUELAS"),
         id = str_replace(id, "BUENOS AIRES_GENERAL MADARIAGA", "BUENOS AIRES_GENERAL JUAN MADARIAGA"),
         id = str_replace(id, "BUENOS AIRES_VEINTICINCO DE MAYO", "BUENOS AIRES_25 DE MAYO")) %>% 
  select(id, cde, prov, geometry)

df <- res %>% 
  left_join(geo, by="id") %>% 
  mutate(Porcentaje = as.numeric(Porcentaje),
         Votos = as.numeric(Votos)) %>% 
  st_as_sf() 

head(df)
Simple feature collection with 6 features and 12 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -63.39 ymin: -38.28 xmax: -58.29 ymax: -34.75
Geodetic CRS:  WGS 84
# A tibble: 6 × 13
  id         Seccion Elecciones Partido Porcentaje Votos Participacion Electores
  <chr>      <chr>   <chr>      <chr>        <dbl> <dbl> <chr>         <chr>    
1 BUENOS AI… 25 De … GENERALES… BLANCO        3.69   925 79            31754    
2 BUENOS AI… 9 De J… GENERALES… BLANCO        2.2    746 78.38         43208    
3 BUENOS AI… Adolfo… GENERALES… BLANCO        3.39   361 73.49         14505    
4 BUENOS AI… Adolfo… GENERALES… BLANCO        5.3    441 76.65         10861    
5 BUENOS AI… Alberti GENERALES… BLANCO        3.68   292 82.21         9648     
6 BUENOS AI… Almira… GENERALES… BLANCO        2.48  8821 77.96         455602   
# ℹ 5 more variables: Votantes <chr>, Provincia <chr>, cde <chr>, prov <chr>,
#   geometry <MULTIPOLYGON [°]>

Sin letra chica

Un problema común al trabajar con grandes territorios es el desequilibro entre importancia de la unidad y el tamaño de su área. A veces, territorios muy importantes no son visibles sólo por ser pequeños. Esto no implica que sean prescindibles: por su población, por su aporte económico o simplemente para no ocultar ningún dato, a veces es necesario hacer un pequeño hack para verlos en pantalla.

Code
main_plot <- df %>% 
  filter(Partido == "UNION POR LA PATRIA") %>% 
  ggplot()+
  geom_sf(aes(fill=Porcentaje), color="black")+
  scale_fill_fermenter(palette = "Blues", direction = 1, n.breaks=5,
                       labels = scales::label_number(suffix = "%"))+ 
  labs(x="", y="", fill="",
       title="Unión por la Patria", 
       subtitle="Elecciones generales 2023", 
       caption="Elaboración propia según datos de datacp.ar")+
  theme_void()+
  theme(plot.caption = element_text(hjust = 0))

main_plot

El mapa anterior tiene una complicación: el conurbano bonaerense, que alberga dos tercios de la población de la provincia, casi no se aprecia en la figura. Es subsanable con la incorporación de minimapas. Un pequeño rodeo: la delimitación de lo que llamamos conurbano es una discusión con múltiples aristas y criterios. Aquí, sólo para evadirla prácticamente, tomaremos un recorte que habilita la división de secciones provinciales de la Provincia de Buenos Aires. Definiremos al conurbano como la primera y tercera sección provincial.

Code
equi <- readxl::read_excel(params$ruta_equi, sheet="pba_seccprov")
head(equi)
# A tibble: 6 × 2
  municipio_id seccion_electoral
         <dbl> <chr>            
1         6854 VII              
2         6588 IV               
3         6007 VI               
4         6014 VI               
5         6021 IV               
6         6028 III              

Tenemos un problema de compatibilidad de tipos. En este caso, nos conviene utilizar números.

Code
# df %>% left_join(equi, by=c("cde"="municipio_id"))

Transformamos y unimos.

Code
df <- df %>% 
  mutate(municipio_id = as.numeric(cde))%>% 
  left_join(equi, by="municipio_id")

Con patchwork podemos unir ambos mapas en un mismo eje.

Code
library(patchwork)

gba_bbox <- df %>% filter(seccion_electoral %in% c("I","III")) %>% st_bbox()

zoom_plot <- df %>% 
  filter(Partido == "UNION POR LA PATRIA") %>% 
  ggplot()+
  geom_sf(aes(fill=Porcentaje), color="black")+
  coord_sf(
    xlim = c(gba_bbox["xmin"], gba_bbox["xmax"]),
    ylim = c(gba_bbox["ymin"], gba_bbox["ymax"]),
    expand = FALSE)+
  scale_fill_fermenter(palette = "Blues", direction = 1, n.breaks=5,
                       labels = scales::label_number(suffix = "%"))+ 
  labs(title="GBA")+
  guides(fill="none")+
  theme_void()

main_plot + inset_element(zoom_plot, left = .92, bottom = .52, right = 1.6, top = 1.2) 

¡Necesito contexto!

Instalamos ggspatial para agregar mapas base.

Code
#install.packages("ggspatial")
#install.packages("prettymapr")

library(ggspatial)

El argumento annotation_* nos va a permitir agregar ciertos elementos al gráfico de ggplot.

Code
df %>% 
  filter(Partido == "UNION POR LA PATRIA") %>% 
  ggplot()+
  annotation_map_tile(zoom=5) +
  annotation_north_arrow(location = "br", which_north = "true",
                         height = unit(.8, "cm"), width = unit(.8, "cm"))+
  geom_sf(aes(fill=Porcentaje), color="black", alpha=.8)+
  scale_fill_fermenter(palette = "Blues", direction = 1, n.breaks=5,
                       labels = scales::label_number(suffix = "%"))+ 
  labs(x="", y="", fill="",
       title="Unión por la Patria", 
       subtitle="Agregamos mapa base con librería ggspatial", 
       caption="Elaboración propia según datos de datacp.ar")+
  theme_void()+
  theme(plot.caption = element_text(hjust = 0))

El argumento zoom nos permite trabajar sobre la calidad del mapa que presentamos. Un número mayor implica más tiempo de procesamiento.

Code
df %>% 
  filter(Partido == "UNION POR LA PATRIA") %>% 
  filter(seccion_electoral %in% c("I", "III", "VIII")) %>% 
  ggplot()+
  annotation_map_tile(zoom=7) + # con zoom=8 o más se rompe el renderizado para web. Probar localmente.
  geom_sf(aes(fill=Porcentaje), color="black", alpha=.8)+
  scale_fill_fermenter(palette = "Blues", direction = 1, n.breaks=3,
                       labels = scales::label_number(suffix = "%"))+ 
  labs(x="", y="", fill="",
       title="Unión por la Patria", 
       subtitle="Agregamos mapa base con librería ggspatial", 
       caption="Elaboración propia según datos de datacp.ar")+
  theme_void()+
  theme(plot.caption = element_text(hjust = 0))

Podríamos probar otros mapas base.

Code
print(rosm::osm.types())
 [1] "osm"                    "opencycle"              "hotstyle"              
 [4] "loviniahike"            "loviniacycle"           "stamenbw"              
 [7] "stamenwatercolor"       "osmtransport"           "thunderforestlandscape"
[10] "thunderforestoutdoors"  "cartodark"              "cartolight"            
Code
df %>% 
  filter(Partido == "UNION POR LA PATRIA") %>%
  filter(seccion_electoral %in% c("I", "III", "VIII")) %>% 
  ggplot()+
  annotation_map_tile(type="cartodark", zoom=7) + # con zoom=8 o más se rompe el renderizado para web. Probar localmente.
  geom_sf(aes(fill=Porcentaje), color="black", alpha=.8)+
  scale_fill_fermenter(palette = "Blues", direction = 1, n.breaks=3,
                       labels = scales::label_number(suffix = "%"))+ 
  labs(x="", y="", fill="",
       title="Unión por la Patria", 
       subtitle="Agregamos mapa base con librería ggspatial", 
       caption="Elaboración propia según datos de datacp.ar")+
  theme_void()+
  theme(plot.caption = element_text(hjust = 0))

Mapas interactivos con leaflet

Ante todo, se instala y carga la librería. leaflet es una librería basada en JavaScript y es ampliamente utilizada en distintos lenguajes de programación para producir mapas interactivos.

Code
#install.packages("leaflet")
library(leaflet)

Calculemos el centroide de Buenos Aires.

Code
sf_use_s2(FALSE)
centroide <- df %>%
  st_make_valid() %>% 
  mutate(prov="PBA") %>% 
  group_by(prov) %>% 
  summarise(geometry = st_union(geometry)) %>% 
  st_centroid() %>% 
  st_coordinates()

centroide
             X         Y
[1,] -60.57121 -36.69671

leaflet sostiene la sintaxis de ggplot. Facilita su uso.

Code
leaflet() %>%
  addTiles() %>% 
  setView(lng = centroide[1], lat = centroide[2], zoom=6)

También permite trabajar con dataframes.

Code
municipios <- c(6574, 6357, 6056, 6490)

df_filtrado <- df %>% filter(Partido=="UNION POR LA PATRIA") %>% filter(municipio_id %in% municipios) %>% st_centroid()


m <- leaflet(df_filtrado) %>% 
  addTiles() %>% 
  addCircleMarkers(radius=~Porcentaje)

m

También se puede trabajar con polígonos.

Code
df_filtrado <- df %>% filter(Partido=="UNION POR LA PATRIA") 


m <- leaflet(df_filtrado) %>% 
  addTiles() %>% 
  addPolygons()

m

Repliquemos el mapa cloroplético.

Code
# paleta de colores
pal <- colorBin("Blues", domain = df_filtrado$Porcentaje, bins = 3)

# construimos labels
df_filtrado <- df_filtrado %>% 
  mutate(label = paste0("<strong>", Seccion, "</strong>", "<br>", round(Porcentaje,1), "%"))

m <- leaflet(df_filtrado) %>% 
  addTiles() %>% 
  addPolygons(
    fillColor = ~pal(Porcentaje),
    weight=1, # ancho de bordes
    opacity=.5, # transparencia de bordes
    color="black", # color de bordes
  fillOpacity = .6, # transparencia de área
  label=~lapply(label, htmltools::HTML)
  ) %>% 
  addLegend(pal=pal, 
            values=~Porcentaje,
            opacity=1) %>% 
  addControl(
    html = "<b>Resultados electorales G2023 por sección</b>",
    position = "topleft"
  )

m

🧪 Práctica corta (15 minutos)

Elegir y resolver alguna de las siguientes consignas.

  • Graficar en mapa de la provincia de Buenos Aires con los resultados de Juntos por el Cambio, eligiendo una paleta de colores apropiada. Agregar un minimapa haciendo zoom en la sección provincial donde mejores resultados tuvo.
  • Probar un mapa interactivo de los resultados de LLA pero cambiando el mapa de fondo. ¿Qué otra información se puede agregar al popup?

3.3. Mapas bivariados

Hay momentos donde, para comprender la información, es necesario sumar variables. En el mundo electoral para entender resultados globales (a nivel provincia en este caso) siempre es útil ver Votos en absolutos y/o electores por cada departamento.

Code
plot_porcentaje <- df_filtrado %>%
  ggplot()+
  geom_sf(aes(fill=Porcentaje), color="black", alpha=.8)+
  scale_fill_fermenter(palette = "Blues", direction = 1, n.breaks=3,
                       labels = scales::label_number(suffix = "%"))+ 
  labs(x="", y="", fill="", title="Unión por la Patria", subtitle="Porcentaje")+
  theme_void()

plot_votos <- df_filtrado %>%
  ggplot()+
  geom_sf(aes(fill=Votos), color="black", alpha=.8)+
  scale_fill_fermenter(palette = "Reds", direction = 1, n.breaks=3,
                       labels = scales::label_number(scale = 1/1000, suffix = "k"))+ 
  labs(x="", y="", fill="", title="Unión por la Patria", subtitle="Votos")+
  theme_void()

plot_porcentaje + plot_votos

Visualmente podría representarse ambas variables a través de los mapas bivariados.

Code
#install.packages("biscale")
#install.packages("cowplot")
library(biscale)

dim <- 2
pal <- "GrPink" # otras paletas: "Bluegill", "BlueGold", "BlueOr", "BlueYl", "Brown"/"Brown2", "DkBlue"/"DkBlue2", "DkCyan"/"DkCyan2", "DkViolet"/"DkViolet2", "GrPink"/"GrPink2", "PinkGrn", "PurpleGrn", or "PurpleOr"
df_biscale <- df_filtrado %>% 
  mutate(Votos = as.numeric(Votos)) %>% 
  bi_class(x="Votos", y="Porcentaje", style="quantile", dim=dim)

map <- ggplot(df_biscale) +
  geom_sf(aes(fill = bi_class), 
          color = "black", 
          show.legend = FALSE) +
  bi_scale_fill(pal = pal, dim = dim) +
  labs(
    title = "Unión por la Patria G2023",
   # subtitle = "% y votos - mapa bivariado",
  ) +
  bi_theme()+
  theme(plot.title = element_text(size = 12),      # título más chico
    plot.subtitle = element_text(size = 10),   # subtítulo más chico
    plot.title.position = "plot"
    )

legend <- bi_legend(pal = pal,
                    dim = dim,
                    xlab = "Más votos",
                    ylab = "Mayor %",
                    size = 8)

map + inset_element(legend, left = .9, bottom = -.7, right = 1.6, top = 1.2) 

3.4. Puntada con hilo

Último tema para este encuentro. Al momento de diseñar nuestra visualización es importante tener a mano los distintos atributos de los que se puede hacer uso. Los puntos son el objeto geográfico que nos permite jugar más con los distintos atributos. Para ayudar a entender la importancia de ellos, se utilizarán los resultados de la elección presidencial del año 2015 en la Provincia de Buenos Aires.

Code
df15 <- readxl::read_excel(params$ruta_pba_g15) %>% 
  left_join(geo, by="id") %>% 
  mutate(Porcentaje = as.numeric(Porcentaje),
         Votos = as.numeric(Votos)) %>% 
  st_as_sf() %>% 
  group_by(id) %>% 
  mutate(
    ganador = Partido[which.max(Porcentaje)]
  ) %>%
  ungroup()

dim(df15)
[1] 1215   14

Una mínima limpieza. Construir la variable de ganador, pasar la geometría a puntos y agregar las coordenadas como variables.

Code
#colores de partidos
colores <- c( "FRENTE PARA LA VICTORIA" = "skyblue",
              "CAMBIEMOS" = "orange",
              "UNIDOS POR UNA NUEVA ALTERNATIVA (UNA)" = "deeppink" )

df15 %>% 
  ggplot()+
  geom_sf(aes(fill=ganador))+
  scale_fill_manual(values=colores, name="ganador")+
  labs(title="Ganador por departamento",
       subtitle="Elecciones presidenciales G2015")+
  theme_void()

Existe un efecto visual dado por la relación entre las áreas de los polígonos y los ganadores: parece que CAMBIEMOS obtuvo más votos.

Code
df15 %>% 
  st_drop_geometry() %>% 
  group_by(Partido) %>% 
  summarise(votos=sum(as.numeric(Votos))) %>% 
  arrange(desc(votos))
# A tibble: 9 × 2
  Partido                                     votos
  <chr>                                       <dbl>
1 FRENTE PARA LA VICTORIA                   3419041
2 CAMBIEMOS                                 3031168
3 UNIDOS POR UNA NUEVA ALTERNATIVA (UNA)    2062610
4 FRENTE DE IZQUIERDA Y DE LOS TRABAJADORES  341734
5 PROGRESISTAS                               264583
6 BLANCO                                     223902
7 COMPROMISO FEDERAL                          89256
8 NULO                                        55030
9 IMPUGNADO                                    7400
Code
df15 %>% 
  st_drop_geometry() %>% 
  distinct(id, ganador) %>% 
  count(ganador) %>% 
  arrange(desc(n))
# A tibble: 3 × 2
  ganador                                    n
  <chr>                                  <int>
1 CAMBIEMOS                                 83
2 FRENTE PARA LA VICTORIA                   50
3 UNIDOS POR UNA NUEVA ALTERNATIVA (UNA)     2

Veamos con puntos al ganador por municipio utilizando puntos.

Code
# transformamos la geometría a puntos
df_point <- df15  %>%  
  st_centroid() %>%
  mutate(
    X = st_coordinates(.)[,1],
    Y = st_coordinates(.)[,2]
  )

# creamos una capa por sección provincial para darle un marco
geo_prov <- st_read(params$ruta_shp) %>% 
  filter(NAM == "Buenos Aires")
Reading layer `ign_provincia' from data source 
  `C:\Users\tomas.bustos\Documents\personal\estacion-r\Mapas en accion\geo\ign_provincia\ign_provincia.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 24 features and 11 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74 ymin: -90 xmax: -25 ymax: -21.78086
Geodetic CRS:  WGS 84
Code
#print(rosm::osm.types())


df_point %>% 
  ggplot()+
  #annotation_map_tile(zoom=5, type="cartolight")+
  geom_sf(aes(color=ganador))+
  scale_color_manual(values=colores, name="ganador")+
  geom_sf(data=geo_prov, alpha=0)+
  labs(title="Ganador por departamento",
       subtitle="Elecciones presidenciales G2015")+
  theme_void()

Se puede jugar con el color, el tamaño y la intensidad.

Code
df_point %>% 
  ggplot()+
  geom_sf(aes(fill = ganador, size = Porcentaje),
          shape = 21, color = "black", alpha = 0.8) +
  scale_fill_manual(values=colores, name="ganador")+
  geom_sf(data=geo_prov, alpha=0)+
  labs(title="Ganador por departamento",
       subtitle="Elecciones presidenciales G2015")+
  theme_void()


🧩 Para seguir practicando

  • Graficar la diferencia del peronismo entre 2025 y 2023.
  • Graficar ganador de las elecciones 2023 pero a nivel región.
  • Graficar la diferencia de La Libertad Avanza entre 2025 y 2023 utilizando una paleta divergente pero con sus valores clasificados en 3 o 5 grupos.